useReducer
Way back in Module 2, I shared my thoughts on global state management tools like Redux. One of the things I said is that React has “absorbed” some of the best parts of Redux.
In this lesson, we're going to talk about reducers, a feature plucked straight out of Redux, and implemented in React with the useReducer
hook.
Now, Redux is known for having a pretty steep learning curve; there are a bunch of abstract concepts and vernacular that we need to become familiarized with. Unfortunately, the useReducer
hook has imported all of that complexity as well. We're going to cover a lot of new ideas in this lesson!
Intro to useReducer
Video Summary
In React, there are two ways to create state variables: the useState
hook, like we've been using throughout this course, and the useReducer
hook. Both hooks allow us to create and update state variables.
The big innovation with useReducer
is that it teases apart two separate ideas:
- The actions that fire in our application, like the user adding a new todo, or marking a todo as completed.
- The state-updating logic that runs when an action occurs.
In this video, I demonstrate how useReducer
works by migrating our Todo list application to use useReducer
instead of useState
. It's a difficult video to fully summarize, but essentially we:
- Create a
reducer
function to hold all of the state-updating logic - Use the
dispatch
function to tell React when an action occurred - Update the 3 bits of business logic — creating a todo, toggling a todo, and deleting a todo — to use this new flow.
The drawback of useReducer
is that there's more scaffolding required. We have to jump through a couple of hoops.
The benefit is that it forces us to group our state-updating logic together. There is no setTodos
function; the only way to change the todos
state is to dispatch an action.
Also, because the reducer
function lives outside our component, we can easily set up unit tests without requiring us to mount any components.
Essentially, useReducer
presents an alternative structure we can use for our state-updating logic. It may not be worth the boilerplate in simple cases, but it does nudge us towards a good structure, and this can really help in complex situations.
Here's the original playground, without useReducer
:
Code Playground
And here's the final code from the video above, with useReducer
:
Code Playground
Note: I made one small tweak to this code from the video. Before, the ID was generated inside the reducer, using crypto.randomUUID()
. In this updated playground, we create that ID in the handleCreateTodo
function, passing it in with the action. This was done to keep the reducer pure; more on this below.
Reducers should be pure functions
One gotcha with reducers is that they must be pure functions.
A pure function is a function that always returns the same output given the same inputs. For example, this is a pure function:
function addNums(a, b) { return a + b;}
When I run the code addNums(1, 1)
, it will return 2
every single time.
By contrast, here's an impure function:
function getRandomNumber() { return Math.random();}
Every time I run the code getRandomNumber()
, I get a different result, since it generates a random value every single time:
getRandomNumber(); // 0.0564345954320411getRandomNumber(); // 0.8482841158362815
When it comes to the useReducer
hook, our reducer functions must be pure. They must always return the same output, given the same input.
It's startlingly easy to make this mistake. In fact, I made this mistake myself, while filming the video for this lesson. 😅
In the video above, I wind up with a reducer that looks like this:
function reducer(todos, action) { if (action.type === 'create-todo') { return [ ...todos, { value: action.value, id: crypto.randomUUID(), }, ]; } // ✂️ Other actions omitted for brevity}
That crypto.randomUUID()
expression produces a different value every time. As a result, we get a different output when calling the function with the same input:
reducer([], { value: 'buy groceries' });// -> [{ value: 'buy-groceries', id: 'c455ebae-db9f-42f7-8ec5-c3a0e169938c' }]
reducer([], { value: 'buy groceries' });// -> [{ value: 'buy-groceries', id: '5f3c38bb-1b32-430e-8282-d489b546ac1b' }]
Here's the solution: We can pass these impure values into the reducer, through the action.
For example:
function reducer(todos, action) { if (action.type === 'create-todo') { return [ ...todos, { value: action.value, id: action.id, }, ]; } // ✂️ Other actions omitted for brevity}
// We would then use it like this:const action = { value: 'buy groceries', id: crypto.randomUUID(),};
reducer([], action);
As I mentioned, it's an easy mistake to make, but fortunately, the solution tends to be pretty straightforward, most of the time.
You can learn more about the useReducer
hook in the official documentation.